'''PDF页面引擎（例如PyMuPDF, pdfminer）的包装器，用于完成以下工作：

* 提取源内容
* 清理块/形状，例如页面外的元素
* 计算页面边距
* 大致解析页面结构，即章节和列
'''

from abc import (ABC, abstractmethod)
from .BasePage import BasePage
from ..layout.Section import Section
from ..layout.Column import Column
from ..shape.Shape import Hyperlink
from ..shape.Shapes import Shapes
from ..layout.Blocks import Blocks
from ..font.Fonts import Fonts
from ..text.TextSpan import TextSpan
from ..common.share import debug_plot
from ..common import constants
from ..common.Collection import Collection


class RawPage(BasePage, ABC):
    '''页面引擎的包装器。'''

    def __init__(self, page_engine=None):
        ''' 初始化页面布局。

        参数:
            page_engine (Object): 源PDF页面。
        '''
        BasePage.__init__(self)
        self.page_engine = page_engine
        self.blocks = Blocks(parent=self)
        self.shapes = Shapes(parent=self)


    @abstractmethod
    def extract_raw_dict(self, **settings):
        '''使用页面引擎提取源数据。返回具有以下结构的字典：
        ```
            {
                "width" : w,
                "height": h,
                "blocks": [{...}, {...}, ...],
                "shapes" : [{...}, {...}, ...]
            }
        ```
        '''


    @property
    def text(self):
        '''此页面中提取的所有文本，图像被视为 ``<image>``。
        应在 ``restore()`` 数据后运行。'''
        return '\n'.join([block.text for block in self.blocks])

    @property
    def raw_text(self):
        '''当前页面中提取的原始文本。应在 ``restore()`` 数据后运行。'''
        return '\n'.join([block.raw_text for block in self.blocks])


    @debug_plot('源文本块')
    def restore(self, **settings):
        '''初始化使用 ``PyMuPDF`` 提取的布局。'''
        raw_dict = self.extract_raw_dict(**settings)
        self.blocks.restore(raw_dict.get('blocks', []))
        self.shapes.restore(raw_dict.get('shapes', []))
        return self.blocks


    @debug_plot('已清理的形状')
    def clean_up(self, **settings):
        '''清理原始块和形状，例如:

        * 删除负值或重复的实例，
        * 检测形状的语义类型
        '''
        # 首先清理块
        self.blocks.clean_up(
            settings['float_image_ignorable_gap'],
            settings['line_overlap_threshold'])
        # 清理形状
        self.shapes.clean_up(
            settings['max_border_width'],
            settings['shape_min_dimension'])
        return self.shapes


    def process_font(self, fonts:Fonts):
        '''更新 ``TextSpan`` 的字体属性，例如字体名称、字体行高比例。

        参数:
            fonts (Fonts): 通过 ``fonttools`` 解析的字体。
        '''
        # 获取所有文本跨度
        spans = []
        for line in self.blocks:
            spans.extend([span for span in line.spans if isinstance(span, TextSpan)])

        # 检查并更新字体名称，行高
        for span in spans:
            font = fonts.get(span.font)
            if not font: continue

            # 使用fonttools解析的字体更新字体属性
            span.font = font.name
            if font.line_height:
                span.line_height = font.line_height * span.size


    def calculate_margin(self, **settings):
        """计算页面边距。

        .. note::
            确保此方法在清理布局后立即运行，以便页面边距是
            基于有效布局计算的，并保持不变。
        """
        # 从形状中排除超链接，因为超链接可能不合理地存在于页面之外，
        # 而它应该始终在页面内，因为它附加到文本上。
        shapes = Shapes([shape for shape in self.shapes if not isinstance(shape, Hyperlink)])

        # 如果不存在块，则返回默认边距
        if not self.blocks and not shapes: return (constants.ITP, ) * 4

        x0, y0, x1, y1 = self.bbox
        u0, v0, u1, v1 = self.blocks.bbox | shapes.bbox

        # 边距
        left = max(u0-x0, 0.0)
        right = max(x1-u1-constants.MINOR_DIST, 0.0)
        top = max(v0-y0, 0.0)
        bottom = max(y1-v1, 0.0)

        # 减少计算的上/下边距以留出一些空间
        top *= settings['page_margin_factor_top']
        bottom *= settings['page_margin_factor_bottom']

        # 如果计算的边距足够大，则使用正常边距
        return (
            min(constants.ITP, round(left, 1)),
            min(constants.ITP, round(right, 1)),
            min(constants.ITP, round(top, 1)),
            min(constants.ITP, round(bottom, 1)))


    def parse_section(self, **settings):
        '''检测并创建页面章节。

        .. note::
            - 目前仅考虑两列章节。
            - 在此步骤之前必须解析页面边距。
        '''
        # 边界框
        X0, Y0, X1, _ = self.working_bbox

        # 收集所有块（行级别）和形状
        elements = Collection()
        elements.extend(self.blocks)
        elements.extend(self.shapes)
        if not elements: return

        # 用收集的行创建章节
        lines = Collection()
        sections = []
        def close_section(num_col, elements, y_ref):
            # 如果都是单列，则附加到最后一个章节
            if sections and sections[-1].num_cols==num_col==1:
                column = sections[-1][0] # type: Column
                column.union_bbox(elements)
                column.add_elements(elements)
            # 否则，创建新章节
            else:
                section = self._create_section(num_col, elements, (X0, X1), y_ref)
                if section:
                    sections.append(section)


        # 逐行检查章节
        pre_num_col = 1
        y_ref = Y0 # 计算章节之间的垂直距离
        for row in elements.group_by_rows():
            # 逐列检查列
            cols = row.group_by_columns()
            current_num_col = len(cols)

            # 列检查:
            # 仅考虑2列
            if current_num_col>2:
                current_num_col = 1

            # 两列的宽度不应有显著差异
            elif current_num_col==2:
                u0, v0, u1, v1 = cols[0].bbox
                m0, n0, m1, n1 = cols[1].bbox
                x0 = (u1+m0)/2.0
                c1, c2 = x0-X0, X1-x0 # 列宽
                w1, w2 = u1-u0, m1-m0 # 行宽
                f = 2.0
                if not 1/f<=c1/c2<=f or w1/c1<0.33 or w2/c2<0.33:
                    current_num_col = 1

            # 处理异常情况
            if pre_num_col==2 and current_num_col==1:
                # 虽然当前行有一个单列，但它可能有另一个虚拟
                # 和空列。如果是这样，它应该被视为2列
                cols = lines.group_by_columns()
                pos = cols[0].bbox[2]
                if row.bbox[2]<=pos or row.bbox[0]>pos:
                    current_num_col = 2

                # pre_num_col!=current_num_col => 用收集的行关闭章节，
                # 在此之前，进一步检查收集的行的高度
                else:
                    x0, y0, x1, y1 = lines.bbox
                    if y1-y0<settings['min_section_height']:
                        pre_num_col = 1


            elif pre_num_col==2 and current_num_col==2:
                # 虽然都是2列，但它们相互不对齐
                combine = Collection(lines)
                combine.extend(row)
                if len(combine.group_by_columns(sorted=False))==1: current_num_col = 1


            # 如果与前一章节的列数不同，完成前一章节
            if current_num_col!=pre_num_col:
                # 处理前一章节
                close_section(pre_num_col, lines, y_ref)
                if sections:
                    y_ref = sections[-1][-1].bbox[3]

                # 开始潜在的新章节
                lines = Collection(row)
                pre_num_col = current_num_col

            # 否则，收集当前行以供进一步处理
            else:
                lines.extend(row)

        # 不要忘记最后一个章节
        close_section(current_num_col, lines, y_ref)

        return sections


    @staticmethod
    def _create_section(num_col:int, elements:Collection, h_range:tuple, y_ref:float):
        '''基于列数、候选元素和水平边界创建章节。'''
        if not elements: return
        X0, X1 = h_range

        if num_col==1:
            x0, y0, x1, y1 = elements.bbox
            # 注意：此处不要直接使用Column((X0, y0, X1, y1))。我们必须设置最终边界框
            # 每次update_bbox以避免在存在页面旋转的情况下双重旋转。
            column = Column().update_bbox((X0, y0, X1, y1)) # 这是最终边界框，必须使用update_bbox
            column.add_elements(elements)
            section = Section(space=0, columns=[column])
            before_space = y0 - y_ref
        else:
            cols = elements.group_by_columns()
            u0, v0, u1, v1 = cols[0].bbox
            m0, n0, m1, n1 = cols[1].bbox
            u = (u1+m0)/2.0

            column_1 = Column().update_bbox((X0, v0, u, v1))
            column_1.add_elements(elements)

            column_2 = Column().update_bbox((u, n0, X1, n1))
            column_2.add_elements(elements)

            section = Section(space=0, columns=[column_1, column_2])
            before_space = v0 - y_ref

        section.before_space = round(before_space, 1)
        return section
